<?php
/**
 * Copyright (c) 2009, RealDolmen
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of RealDolmen nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY RealDolmen ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL RealDolmen BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category   Microsoft
 * @package    Microsoft_WindowsAzure_Storage
 * @subpackage Blob
 * @copyright  Copyright (c) 2009, RealDolmen (http://www.realdolmen.com)
 * @license    http://todo     name_todo
 * @version    $Id: Blob.php 24511 2009-07-28 09:17:56Z unknown $
 */

/**
 * @see Microsoft_WindowsAzure_Storage_Blob
 */
require_once 'Microsoft/WindowsAzure/Storage/Blob.php';

/**
 * @see Microsoft_WindowsAzure_Exception
 */
require_once 'Microsoft/WindowsAzure/Exception.php';


/**
 * @category   Microsoft
 * @package    Microsoft_WindowsAzure_Storage
 * @subpackage Blob
 * @copyright  Copyright (c) 2009, RealDolmen (http://www.realdolmen.com)
 * @license    http://phpazure.codeplex.com/license
 */
class Microsoft_WindowsAzure_Storage_Blob_Stream
{
    
    // cached data
    private static $_blobInstanceCache;
    private static $_containerExistsCache;
    
    /**
     * Current file name
     * 
     * @var string
     */
    private $_fileName = null;
    
    /**
     * Temporary file name
     * 
     * @var string
     */
    private $_temporaryFileName = null;
    
    /**
     * Temporary file handle
     * 
     * @var resource
     */
    private $_temporaryFileHandle = null;
    
    /**
     * Blob storage client
     * 
     * @var Microsoft_WindowsAzure_Storage_Blob
     */
    private $_storageClient = null;
    
    /**
     * Write mode?
     * 
     * @var boolean
     */
    private $_writeMode = false;
    
    /**
     * List of blobs
     * 
     * @var array
     */
    private $_dirBlobs = null;
    
    private function invalidateCache($path)
    {
        $containerName = strtolower($this->getContainerName($path));
        if (self::$_blobInstanceCache == null)
            self::$_blobInstanceCache = array();
            
        if (self::$_containerExistsCache == null)
            self::$_containerExistsCache = array();
        
        if (array_key_exists($containerName, self::$_containerExistsCache))
            unset(self::$_containerExistsCache[$containerName]);
        
        if (array_key_exists($containerName, self::$_blobInstanceCache))
            unset(self::$_blobInstanceCache[$containerName]);
    }
    
    private function getBlobInstance($path, $useCache = true)
    {
        if (self::$_blobInstanceCache == null)
            self::$_blobInstanceCache = array();
        
        $containerName = strtolower($this->getContainerName($path));
        if (array_key_exists($containerName, self::$_blobInstanceCache) == false || $useCache !== true)
        {
            self::$_blobInstanceCache[$containerName] = $this->getStorageClient($path)->listBlobs($this->getContainerName($path));
        }
        
        foreach (self::$_blobInstanceCache[$containerName] as $blobInstance)
        {
            if (strtolower($blobInstance->Name) == strtolower($this->getFileName($path)))
            {
                return $blobInstance;
            }
        }
        
        return null;
    }
    
    private function containerExists($path, $useCache = true)
    {
        if (self::$_containerExistsCache == null)
            self::$_containerExistsCache = array();
        
        $containerName = strtolower($this->getContainerName($path));
        if (array_key_exists($containerName, self::$_containerExistsCache) == false || $useCache !== true)
        {
            $containerList = $this->getStorageClient($path)->listContainers();
            foreach ($containerList as $containerItem)
            {
                self::$_containerExistsCache[$containerItem->Name] = $containerItem;
            }
        }
        
        if (array_key_exists($containerName, self::$_containerExistsCache))
        {
            return true;
        }
        
        return false;
    }
    
    
    /**
     * Retrieve storage client for this stream type
     * 
     * @param string $path
     * @return Microsoft_WindowsAzure_Storage_Blob
     */
    protected function getStorageClient($path = '')
    {
        if (is_null($this->_storageClient))
        {
            $url = explode(':', $path);
            if (!$url)
            {
                throw new Microsoft_WindowsAzure_Exception('Could not parse path "' . $path . '".');
            }

            $this->_storageClient = Microsoft_WindowsAzure_Storage_Blob::getWrapperClient($url[0]);
            if (!$this->_storageClient)
            {
                throw new Microsoft_WindowsAzure_Exception('No storage client registered for stream type "' . $url[0] . '://".');
            }
        }
        
        return $this->_storageClient;
    }
    
    /**
     * Extract container name
     *
     * @param string $path
     * @return string
     */
    protected function getContainerName($path)
    {
        $url = parse_url($path);
        if ($url['host']) {
            return $url['host'];
        }

        return '';
    }
    
    /**
     * Extract file name
     *
     * @param string $path
     * @return string
     */
    protected function getFileName($path)
    {
        $url = parse_url($path);
        if ($url['host']) {
            $fileName = isset($url['path']) ? $url['path'] : $url['host'];
    	    if (strpos($fileName, '/') === 0)
    	        $fileName = substr($fileName, 1);
            return str_replace("/", "___", $fileName);
        }
        
        return '';
    }
    
    /**
     * Open the stream
     *
     * @param  string  $path
     * @param  string  $mode
     * @param  integer $options
     * @param  string  $opened_path
     * @return boolean
     */
    public function stream_open($path, $mode, $options, $opened_path)
    {
        $url = parse_url($path);
        $this->_fileName = $url['scheme'] . "://" . $url['host'] . "/" . $this->getFileName($path);
        $this->_temporaryFileName = tempnam(sys_get_temp_dir(), 'azure');
        
        // Check the file can be opened
        $fh = @fopen($this->_temporaryFileName, $mode);
        if ($fh === false)
            return false;
        fclose($fh);
        
        // Write mode?
        if (strpbrk($mode, 'wax+'))
            $this->_writeMode = true;
        else
            $this->_writeMode = false;
        
        // If read/append, fetch the file
        if (!$this->_writeMode || strpbrk($mode, 'ra+'))
        {
            try
            {
                // read the blob into the temp filename
                $this->getStorageClient($this->_fileName)->getBlob(
                    $this->getContainerName($this->_fileName),
                    $this->getFileName($this->_fileName),
                    $this->_temporaryFileName
                );
            }
            catch (Microsoft_WindowsAzure_Exception $ex)
            {
                // do nothing. the temp filename is already created
            }
        }
        
        // Open temporary file handle
        $this->_temporaryFileHandle = fopen($this->_temporaryFileName, $mode);
        
        // Ok!
        return true;
    }

    /**
     * Close the stream
     *
     * @return void
     */
    public function stream_close()
    {
        $this->invalidateCache($this->_fileName);
        @fclose($this->_temporaryFileHandle);
        
        // Upload the file?
        if ($this->_writeMode)
        {
            // Make sure the container exists
            $containerExists = $this->containerExists($this->_fileName);
            
            if (!$containerExists)
            {
                $this->getStorageClient($this->_fileName)->createContainer(
                    $this->getContainerName($this->_fileName)
                );
            }
            
            // Upload the file
            try
            {
                $this->getStorageClient($this->_fileName)->putBlob(
                    $this->getContainerName($this->_fileName),
                    $this->getFileName($this->_fileName),
                    $this->_temporaryFileName
                );
            }
            catch (Microsoft_WindowsAzure_Exception $ex)
            {
                @unlink($this->_temporaryFileName);
                unset($this->_storageClient);
                
                throw $ex;
            }
        }
        
        @unlink($this->_temporaryFileName);
        unset($this->_storageClient);
    }

    /**
     * Read from the stream
     *
     * @param  integer $count
     * @return string
     */
    public function stream_read($count)
    {
        if (!$this->_temporaryFileHandle)
        {
            return false;
        }

        return fread($this->_temporaryFileHandle, $count);
    }

    /**
     * Write to the stream
     *
     * @param  string $data
     * @return integer
     */
    public function stream_write($data)
    {
        if (!$this->_temporaryFileHandle)
        {
            return 0;
        }
        
        $len = strlen($data);
        fwrite($this->_temporaryFileHandle, $data, $len);
        return $len;
    }

    /**
     * End of the stream?
     *
     * @return boolean
     */
    public function stream_eof()
    {
        if (!$this->_temporaryFileHandle)
        {
            return true;
        }

        return feof($this->_temporaryFileHandle);
    }

    /**
     * What is the current read/write position of the stream?
     *
     * @return integer
     */
    public function stream_tell()
    {
        return ftell($this->_temporaryFileHandle);
    }

    /**
     * Update the read/write position of the stream
     *
     * @param  integer $offset
     * @param  integer $whence
     * @return boolean
     */
    public function stream_seek($offset, $whence)
    {
        if (!$this->_temporaryFileHandle)
        {
            return false;
        }
        
        return (fseek($this->_temporaryFileHandle, $offset, $whence) === 0);
    }

    /**
     * Flush current cached stream data to storage
     *
     * @return boolean
     */
    public function stream_flush()
    {
        $result = fflush($this->_temporaryFileHandle);
        
         // Upload the file?
        if ($this->_writeMode)
        {
            // Make sure the container exists
            $containerExists = $this->containerExists($this->_fileName);
            
            if (!$containerExists)
            {
                $this->getStorageClient($this->_fileName)->createContainer(
                    $this->getContainerName($this->_fileName)
                );
            }
            
            // Upload the file
            try
            {
                $this->getStorageClient($this->_fileName)->putBlob(
                    $this->getContainerName($this->_fileName),
                    $this->getFileName($this->_fileName),
                    $this->_temporaryFileName
                );
            }
            catch (Microsoft_WindowsAzure_Exception $ex)
            {
                @unlink($this->_temporaryFileName);
                unset($this->_storageClient);
                
                throw $ex;
            }
        }
        
        return $result;
    }

    /**
     * Returns data array of stream variables
     *
     * @return array
     */
    public function stream_stat()
    {
        if (!$this->_temporaryFileHandle)
        {
            return false;
        }

        $stat = array();
        $stat['dev'] = 0;
        $stat['ino'] = 0;
        $stat['mode'] = 0;
        $stat['nlink'] = 0;
        $stat['uid'] = 0;
        $stat['gid'] = 0;
        $stat['rdev'] = 0;
        $stat['size'] = 0;
        $stat['atime'] = 0;
        $stat['mtime'] = 0;
        $stat['ctime'] = 0;
        $stat['blksize'] = 0;
        $stat['blocks'] = 0;

        $info = null;
        try {
            $info = $this->getStorageClient($this->_fileName)->getBlobInstance(
                        $this->getContainerName($this->_fileName),
                        $this->getFileName($this->_fileName)
                    );
        }
        catch (Microsoft_WindowsAzure_Exception $ex)
        {
            // Unexisting file...
        }
        if (!is_null($info))
        {
            $stat['size']  = $info->Size;
            $stat['atime'] = time();
        }     
        
        return $stat;
    }

    /**
     * Attempt to delete the item
     *
     * @param  string $path
     * @return boolean
     */
    public function unlink($path)
    {
        $this->invalidateCache($path);
        $this->getStorageClient($path)->deleteBlob(
            $this->getContainerName($path),
            $this->getFileName($path)
        );
    }

    /**
     * Attempt to rename the item
     *
     * @param  string  $path_from
     * @param  string  $path_to
     * @return boolean False
     */
    public function rename($path_from, $path_to)
    {
        
        if ($this->getContainerName($path_from) != $this->getContainerName($path_to))
            throw new Microsoft_WindowsAzure_Exception('Container name can not be changed.');
        
        if ($this->getFileName($path_from) == $this->getContainerName($path_to))
            return true;
        
        $this->invalidateCache($path_from);
        
        $this->getStorageClient($path_from)->copyBlob(
            $this->getContainerName($path_from),
            $this->getFileName($path_from),
            $this->getContainerName($path_to),
            $this->getFileName($path_to)
        );
        $this->getStorageClient($path_from)->deleteBlob(
            $this->getContainerName($path_from),
            $this->getFileName($path_from)
        );
        return true;
    }
    
    /**
     * Return array of URL variables
     *
     * @param  string $path
     * @param  integer $flags
     * @return array
     */
    public function url_stat($path, $flags)
    {
        /*
        If you plan to use your wrapper in a require_once you need to define stream_stat().  If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat().
        stream_stat() must define the size of the file, or it will never be included.  url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work.
        It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666.  If you wish the file to be executable, use 7s instead of 6s.  The last 3 digits are exactly the same thing as what you pass to chmod.  040000 defines a directory, and 0100000 defines a file.  It would be really helpful to add this to the official manual!
        */
        
        $stat = array();
        $stat['dev'] = 0;
        $stat['ino'] = 0;
        $stat['mode'] = 0;
        $stat['nlink'] = 0;
        $stat['uid'] = 0;
        $stat['gid'] = 0;
        $stat['rdev'] = 0;
        $stat['size'] = 0;
        $stat['atime'] = 0;
        $stat['mtime'] = 0;
        $stat['ctime'] = 0;
        $stat['blksize'] = 0;
        $stat['blocks'] = 0;
        
        
        // check if the container exists first
		$containerExists = $this->containerExists($path);
		if (!$containerExists) return false;
        if ($containerExists && $this->getFileName($path) == $this->getContainerName($path))
        {
            $stat['atime'] = time();
            $stat['mode'] = 040777;
            return $stat;
        }
        
        $info = null;
        try
        {   
            $info = $this->getBlobInstance($path);
            if ($info == null) return false;
        }
        catch (Microsoft_WindowsAzure_Exception $ex)
        {
			// does not exist.
            return false;
        }
        if (!is_null($info))
        {
            
            $mod = 0100666;
            try
            {
                //var_dump($info->Metadata);
                if ($info->ContentEncoding == 'simulated-folder')
                {
                    $mod = 040777;
                }
            }
            catch (Exception $ex)
            {
                // no metadata
            }
            
            $stat['mode'] = $mod;
            $stat['size']  = $info->Size;
            $stat['atime'] = time();
        }
        
        return $stat;
    }

    /**
     * Create a new directory
     *
     * @param  string  $path
     * @param  integer $mode
     * @param  integer $options
     * @return boolean
     */
    public function mkdir($path, $mode, $options)
    {
        $this->invalidateCache($path);
		$containerExists = $this->containerExists($path);
        
        if (!$containerExists)
        {   
            // Create container
            try
            {
                $this->getStorageClient($path)->createContainer(
                    $this->getContainerName($path)
                );
            }
            catch (Microsoft_WindowsAzure_Exception $ex)
            {
                return false;
            }
        }
        
        if ($this->getFileName($path) != $this->getContainerName($path))
        {
            // Create dir-simulating blob
            try
            {
                $tmpFileName = tempnam(sys_get_temp_dir(), 'azureDir');
                file_put_contents($tmpFileName, $path);
                
                $this->getStorageClient($path)->putBlob(
                    $this->getContainerName($path),
                    $this->getFileName($path),
                    $tmpFileName,
                    array(),
                    array('Content-Encoding' => 'simulated-folder')
                );
            }
            catch (Microsoft_WindowsAzure_Exception $ex)
            {
                return false;
            }
        }
        
        return true;
    }

    /**
     * Remove a directory
     *
     * @param  string  $path
     * @param  integer $options
     * @return boolean
     */
    public function rmdir($path, $options)
    {
        $this->invalidateCache($path);
        $containerExists = $this->containerExists($path);
        
        if ($containerExists)
        {
            // Delete container
            try
            {
                $this->getStorageClient($path)->deleteContainer(
                    $this->getContainerName($path)
                );
            }
            catch (Microsoft_WindowsAzure_Exception $ex)
            {
                return false;
            }
        }
        else
        {
            // delete dir-simulating blob and all blobs starting with that path
            try
            {
                $blobsInfo = $this->getStorageClient($path)->listBlobs(
                    $this->getContainerName($path));
                
                foreach ($blobsInfo as $blobInfo)
                {
                    if (strtolower($blobInfo->Name) == strtolower($this->getFileName($path)))
                    {
                        $this->getStorageClient($path)->deleteBlob(
                            $this->getContainerName($path),
                            $blobInfo->Name
                        );
                        
                        continue;
                    }
                    
                    if (stristr(strtolower($blobInfo->Name), strtolower($this->getFileName($path)) . '___') !== false)
                    {
                        $this->getStorageClient($path)->deleteBlob(
                            $this->getContainerName($path),
                            $blobInfo->Name
                        );   
                    }
                }
                
            }
            catch (Microsoft_WindowsAzure_Exception $ex)
            {
                return false;
            }
        }
        
        return true;
    }

    /**
     * Attempt to open a directory
     *
     * @param  string $path
     * @param  integer $options
     * @return boolean
     */
    public function dir_opendir($path, $options)
    {
        $containerName = $this->getContainerName($path);
        $blobs = $this->getStorageClient($path)->listBlobs($containerName);
        
        $this->_dirBlobs = array();
        $folderPrefix = $this->getFileName($path);
        $retVal = false;
        
        foreach ($blobs as $blobInfo)
        {
            // at least 1 blobinfo object needs to have the same name asthe folder prefix
            if ($blobInfo->Name == $folderPrefix)
            {
                $retVal = true;
                continue;
            }
            
            // add the blobinfos to the collection only if they start with the given folder prefix.
            if (strpos($blobInfo->Name, $folderPrefix) === 0)
                $this->_dirBlobs[] = ltrim(str_replace($folderPrefix, '', $blobInfo->Name), "_");
        }
        
        return $retVal;
    }

    /**
     * Return the next filename in the directory
     *
     * @return string
     */
    public function dir_readdir()
    {
        $object = current($this->_dirBlobs);
        if ($object !== false) {
            next($this->_dirBlobs);
            return $object;
        }
        return false;
    }

    /**
     * Reset the directory pointer
     *
     * @return boolean True
     */
    public function dir_rewinddir()
    {
        reset($this->_dirBlobs);
        return true;
    }

    /**
     * Close a directory
     *
     * @return boolean True
     */
    public function dir_closedir()
    {
        $this->_dirBlobs = array();
        return true;
    }
}
